# -*- coding: utf-8 -*-
'''
Created on 25.03.2011

@author: aim
'''
import re
import os
import socket
import threading
import xml.dom.minidom


class CPSError(Exception):
    '''Базовый класс для всех исключений в этом модуле'''
    pass


class CPSFileError(CPSError):
    '''Ошибка файловой операции'''
    pass


class CPSWrongIDError(CPSError):
    '''Не найден ID'''
    pass


class CPSWrongMessageFormat(CPSError):
    '''Неправильный формат сообщения синхронизации'''
    pass


class CPSConnectionLostError(CPSError):
    '''Соединение потеряно'''
    pass


class CPSPanel:
    _client = None
    
    def __init__(self, panel_dir):
        self._handlers = {}
        panel_xml_path = os.path.join(panel_dir, 'panel.xml')
        if not os.path.isfile(panel_xml_path):
            raise CPSFileError('"' + panel_xml_path + '" — файл не найден')
        panel_xml = xml.dom.minidom.parse(panel_xml_path)
        panel_tag = panel_xml.getElementsByTagName('panel')[0]
        self._id = panel_tag.getAttribute('id')
        self._name = panel_tag.getAttribute('name')
        elements = panel_tag.childNodes
        self._controls = {}
        for element in elements:
            if element.nodeType == element.ELEMENT_NODE:
                if element.hasAttribute('id'):
                    id = element.getAttribute('id')
                    if element.hasAttribute('value'):
                        value = element.getAttribute('value')
                    else:
                        value = 0
                    self._controls[id] = value

    def _connect(self, client):
        self._client = client
    
    def _disconnect(self):
        self._client = None

    def get_value(self, control_id):
        if not self._controls.has_key(control_id):
            raise CPSWrongIDError('"' + control_id + 
                                  '" — id не найден в панели "' + 
                                  self._id + '"')
        return self._controls[control_id]

    def set_value(self, control_id, value):
        if not self._controls.has_key(control_id):
            raise CPSWrongIDError('"' + control_id + 
                                  '" — id не найден в панели "' + 
                                  self._id + '"')
        self._controls[control_id] = value
        if self._handlers.has_key(control_id):
            self._handlers[control_id](value)
        if self._client != None:
            self._client.send_message(control_id, value)
            
    def set_value_silent(self, control_id, value):
        if not self._controls.has_key(control_id):
            raise CPSWrongIDError('"' + control_id + 
                                  '" — id не найден в панели "' + 
                                  self._id + '"')
        self._controls[control_id] = value
        if self._handlers.has_key(control_id):
            self._handlers[control_id](value)

    def set_handler(self, control_id, func):
        if self._controls.has_key(control_id):
            self._handlers[control_id] = func

    def get_id(self):
        return self._id

    def get_name(self):
        return self._name


class ClientThread(threading.Thread):
    END_LINE = '\n'
    working = True
    _unsend_msg = []
    _event_index = 0

    def __init__(self, channel, details, cps):
        self._channel = channel
        self._details = details
        self._buf = ''
        self._cps = cps
        self._channel.setblocking(1)
        threading.Thread.__init__(self)

    def run(self):
        self.working = True
        print 'Received connection:', self._details[0]
        # Отправка кол-ва панелей
        self._send_string(str(len(self._cps._panels)))
        # Для каждой панели отправка идентификатора и полного названия
        for panel in self._cps._panels.values():
            self._send_string(panel.get_id())
            self._send_string(panel.get_name())
        # Прием идентификатора выбранной панели
        selected_panel_id = self._recv_string()
        selected_panel_id = selected_panel_id[:-len(self.END_LINE)]
        dir_name = os.path.join(self._cps.get_dir(), selected_panel_id)
        if not os.path.isdir(dir_name):
            raise CPSFileError('"' + dir_name + '" — директория не найдена')
        list_dir = os.listdir(dir_name)
        num_files = len(list_dir)
        for file_name in list_dir:
            if file_name[0] == '.':
                num_files -= 1
        # Отправлка кол-ва файлов
        self._send_string(str(num_files))
        for file_name in list_dir:
            if file_name[0] != '.':
                # Отправка имени файла
                self._send_string(file_name)
                path = os.path.join(dir_name, file_name)
                # Отправка размера файла
                self._send_string(str(os.path.getsize(path)))
                f = open(path, 'rb')
                # Отправка содержимого файла порциями по 1 Кб
                while True:
                    buf = f.read(1024)
                    if len(buf) == 0:
                        break
                    self._send_bytes(buf)
        self._panel = self._cps.get_panel(selected_panel_id)
        self._panel._connect(self)
        self._do_work_cicle()    
        self._panel._disconnect()
        self._channel.close()
        print 'Closed connection:', self._details[0]

    def _send_string(self, msg):
        self._channel.sendall(msg + self.END_LINE)

    def _send_bytes(self, msg):
        self._channel.sendall(msg)

    def _recv_string(self):
        while 1:
            try:
                end_pos = self._buf.find(self.END_LINE)
                if end_pos != -1:
                    end_pos += len(self.END_LINE)
                    msg = self._buf[:end_pos]
                    self._buf = self._buf[end_pos:]
                    return msg
                str = self._channel.recv(1024)
                if str == '':
                    raise CPSConnectionLostError()
                self._buf = self._buf + str
            except socket.timeout:
                pass

    def send_message(self, control_id, new_value):
        self._unsend_msg.append((control_id, new_value))
    
    def _do_work_cicle(self):
        self._channel.setblocking(0)
        while 1:
            try:
                if len(self._unsend_msg) > 0:
                    (control_id, new_value) = self._unsend_msg.pop(0)
                    self._send_string('%d %s %d' % (self._event_index, control_id, new_value))
                    self._event_index += 1
                end_pos = self._buf.find(self.END_LINE)
                if end_pos != -1:
                    end_pos += len(self.END_LINE)
                    msg = self._buf[:end_pos]
                    self._buf = self._buf[end_pos:]
                    self._process_msg(msg)
                str = self._channel.recv(1024)
                if str == '':
                    return
                self._buf = self._buf + str
            except:
                pass
            
    def _process_msg(self, msg):
        res = re.match(r'(?P<index>\d+) (?P<id>\w+) (?P<val>-?\d+).*', msg)
        if res == None:
            raise CPSWrongMessageFormat()
        self._event_index = int(res.group('index'))
        self._panel.set_value_silent(res.group('id'), int(res.group('val')))


class CPSServerListener(threading.Thread):
    working = True

    def __init__(self, port, cps):
        self._port = port
        self._cps = cps
        threading.Thread.__init__(self)

    def run(self):
        server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server.bind(('', self._port))
        server.settimeout(1)
        server.listen(5)
        while self.working:
            try:
                channel, details = server.accept()
                ClientThread(channel, details, self._cps).start()
            except socket.timeout:
                pass


class CPSServer:
    def __init__(self, cps_dir):
        self._dir = cps_dir
        # Проверка директории и создание объектов для каждой панели
        if not os.path.isdir(cps_dir):
            raise CPSFileError('"' + cps_dir + '" — директория не найдена')
        self._panels = {}
        for name in os.listdir(cps_dir):
            if name[0] != '.':
                path = os.path.join(cps_dir, name)
                if os.path.isdir(path):
                    panel = CPSPanel(path)
                    self._panels[panel.get_id()] = panel

    def start(self, port):
        # Запуск сервера в новом потоке
        self._listener = CPSServerListener(port, self)
        self._listener.start()

    def stop(self):
        self._listener.working = False

    def get_panel(self, panel_id):
        if not self._panels.has_key(panel_id):
            raise CPSWrongIDError('"' + panel_id + '" — id не найден')
        return self._panels[panel_id]
    
    def get_dir(self):
        return self._dir
